/**
 * 
 */
package com.ordobill.remote.communication.resource.ftp;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.SocketException;
import java.util.ArrayList;

import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.ftp.*;

import com.ordobill.remote.communication.resource.CommunicationResource;

/**
 * @author Choi Jin Wook(A.K.A Brian Choi @ Ordobill Office) / choijinwook84@gmail.com
 *
 */
public class CommunicationByFtp {

    //서버에서 돌아오는 응답 코드를 담습니다.
    private int signFromServer;
    
    //서버와 통신하기 위한 기본정보들을 담습니다.
    private CommunicationResource cr;
    
    //파일 리스트를 담습니다.
    private String[] ftpFileNameStringArray;
    
    //CommunicationResource에서 입력했던 Path정보를 기초로 해당 원격 폴더의 파일 이름을 {@code ArrayList<String>}로 반환합니다.
    private ArrayList<String> ftpFileNameArrayList = new ArrayList<String>();
    
    //CommunicationResource에서 입력했던 Path정보를 기초로 해당 원격 폴더의 파일 목록을 {@code ArrayList<FTPFile>}로 반환합니다.
    private ArrayList<FTPFile> ftpFilesArrayList = new ArrayList<FTPFile>();
    
    //FTP로 통신하기 위한 클래스입니다.
    private NewFTPClient ftpClient = new NewFTPClient();
    
    
    /**
     * FTP를 통한 통신 클래스의 생성자입니다.
     * 
     * @param cr {@code CommunicationResource}
     */
    public CommunicationByFtp(CommunicationResource cr) {
        
        this.cr = cr;
        
        this.makeConnection(this.cr);
        
    }
    
    /**
     * FTP를 통한 통신 클래스의 생성자입니다.
     */
    public CommunicationByFtp() { }
    
    /**
     * 연결을 만듭니다.
     * 
     * @param cr
     */
    public void makeConnection(CommunicationResource cr){
        
        ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
        
        try {
            //포트 설정이 null이거나 없다면 기본 포트인 21번을 입력해 줍니다.
            if(cr.getHostPort() == 0){
                cr.setHostPort(21);
            }
            
            //항상 호스트 주소와 포트를 건내줍니다.
            ftpClient.connect(cr.getHostName(), cr.getHostPort());
            
            //통신 모드에서 문자셋이 null이거나 입력되어있지 않다면 
            //FTP 클래스에서 제공되는 DEFAULT_CONTROL_ENCODING:ISO-8859-1을 입력합니다. 
            if(cr.getCharacterCodeSet() == null || cr.getCharacterCodeSet().equals("")){
                cr.setCharacterCodeSet(FTP.DEFAULT_CONTROL_ENCODING);
            }
            
            //인코딩에 문자셋을 입력하여 줍니다.
            //Window의 경우 MS949가 일반적입니다.
            ftpClient.setControlEncoding(cr.getCharacterCodeSet());
            
            //서버에서 응답을 받습니다.
            signFromServer = ftpClient.getReplyCode();
            
            //서버 응답을 체크하여 연결되는지 여부를 확인합니다.
            if(!FTPReply.isPositiveCompletion(signFromServer)) {
                ftpClient.disconnect();
                System.err.println("FTP server refused connection.");
                System.exit(1);
            }
            
            //호스트 서버의 URL로 연결이 되었다면 이제 로그인을 합니다.
            if(!ftpClient.login(cr.getUserName(), cr.getUserPw())){
                ftpClient.logout();
            }
            
            //호스트 서버에서 오는 로그인 결과를 받습니다.
            signFromServer = ftpClient.getReplyCode();
            
            this.ftpFileNameStringArray = this.getFileList();
            
            boolean downComplete = this.getObjectFile(cr.getObjectFileName());

            ftpClient.logout();
        } catch (SocketException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(ftpClient.isConnected()) {
                try {
                    ftpClient.disconnect();
                } catch(IOException ioe) {
                  // do nothing
                }
            }
        }
    }
    
    /**
     * CommunicationResource에서 입력했던 Path정보를 기초로
     * 해당 원격 폴더의 파일 이름을 반환합니다.
     * 
     * @return fileNames Path정보를 기초로 원격 서버의 폴더의 파일 이름을 {@code String Array}로 반환합니다.
     */
    private String[] getFileList() throws IOException{
        ftpFilesArrayList = ftpFiles(ftpClient, cr.getObjectPathName());
        String[] fileNames = new String[ftpFilesArrayList.size()];
        int _strArr = 0;
        
        for(FTPFile fFile : ftpFilesArrayList){
            fileNames[_strArr] = fFile.getName();
            _strArr++;
        }
        return fileNames;
    }
    
    /**
     * {@code FTPFile} 객체의 {@code ArrayList}형태로 반환 하는 메소드입니다. 
     * 
     * @param fClient FTPClient의 Connection이 맺어진 이 후의 객체입니다.
     * @param pwd 조회하고자 하는 곳의 위치를 표현한 위치(Path {@code String})입니다.
     * @return files 조회하고자 하는 곳의 {@code FTPFile}의 {@code ArrayList}형태로 반환된 instance입니다.
     */
    private ArrayList<FTPFile> ftpFiles(NewFTPClient fClient, String pwd) throws IOException{

        if(pwd == null || pwd.equals("")){
            pwd = "/";
        }
        
        //파일의 위치가 존재하는지 확인 합니다.
        signFromServer = fClient.cwd(pwd);
        if(signFromServer == 550){
            throw new IOException("디렉토리 목록 조회 실패");
        }

        FTPFile[] fArrayList = fClient.listFiles(pwd);
        
        
        //System.out.println(this.getClass().getName()+","+signFromServer);
        for(FTPFile f : fArrayList){
            ftpFilesArrayList.add(f);
        }
            
        return ftpFilesArrayList;
    }
    
    /**
     * 
     * @param fileName
     * @return
     */
    private boolean getObjectFile(String fileName) throws IOException{
        
        if(cr.getTargetPathName()==null || cr.getTargetPathName().equals("")){
            throw new IOException("다운로드할 로컬 디렉토리 위치가 정해지지 않았습니다.");
        }
        
        if(fileName == null || fileName.equals("")){
            throw new IOException("다운로드할 파일의 이름이 정해지지 않았습니다.");
        }
        
        boolean isSuccess = false;
        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
        ftpClient.enterLocalPassiveMode();
        ftpClient.changeWorkingDirectory(cr.getObjectPathName());
        
        File file = new File(cr.getTargetPathName(), fileName);
        
        FTPFile fFile = ftpClient.listFiles(cr.getObjectPathName()+"/"+cr.getObjectFileName())[0];
        FileOutputStream fileOutStream = null;
//        ftpClient.setControlKeepAliveTimeout(0);
//        ftpClient.setConnectTimeout(1000*60*3);
        try{
            fileOutStream = new FileOutputStream(file);
            isSuccess = ftpClient.retrieveFile(cr.getObjectFileName(), fileOutStream, fFile.getSize());
        }catch(IOException ioEx){
            ioEx.printStackTrace();
        }finally{
            if(fileOutStream != null){
                try{
                    fileOutStream.close();
                }catch(IOException ioEx){
                    ioEx.printStackTrace();
                }
            }
        }
        
        //System.out.println(ftpClient);
        this.signFromServer = ftpClient.getReply();
        
        return isSuccess;
    }

    /**
     * 서버에서 응답하는 코드를 반환합니다.
     * 
     * @return signFromServer 서버에서 응답하는 코드 값({@code int})를 반환합니다.
     */
    public int getSignFromServer(){
        return signFromServer;
    }
    
    /**
     * CommunicationResource에서 입력했던 Path정보를 기초로 해당 원격 폴더의 파일 이름을 {@code String Array}로 반환합니다.
     * 
     * @return Path정보를 기초로 원격 서버의 폴더의 파일 이름을 {@code String Array}로 반환합니다.
     */
    public String[] fileNameStringArray(){
        return ftpFileNameStringArray;
    }
    
    /**
     * CommunicationResource에서 입력했던 Path정보를 기초로 해당 원격 폴더의 파일 이름을 {@code ArrayList<String>}로 반환합니다.
     * 
     * @return Path정보를 기초로 해당 원격 폴더의 파일 이름을 {@code ArrayList<String>}로 반환합니다.
     */
    public ArrayList<String> fileNameStringArrayList(){
        for(FTPFile ftpFile : ftpFilesArrayList){
            ftpFileNameArrayList.add(ftpFile.getName());
        }
        return ftpFileNameArrayList;
    }
    
    /**
     * CommunicationResource에서 입력했던 Path정보를 기초로 해당 원격 폴더의 파일 목록을 {@code ArrayList<FTPFile>}로 반환합니다.
     * 
     * @return Path정보를 기초로 해당 원격 폴더의 파일 목록을 {@code ArrayList<FTPFile>}로 반환합니다.
     */
    public ArrayList<FTPFile> ftpFileArrayList(){
        return ftpFilesArrayList;
    }

}
